// @ts-check
// rollup.dts.config.js - Vue3 项目的 TypeScript 声明文件构建配置
// 该文件负责将各个包的 TypeScript 声明文件合并打包成单一的 .d.ts 文件
// 同时处理类型导出优化和特殊的类型文件生成需求

// 导入依赖模块
import assert from 'node:assert/strict' // 严格断言，用于验证操作
import { parse } from '@babel/parser' // Babel 解析器，用于解析 TypeScript AST
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' // Node.js 文件系统操作
import MagicString from 'magic-string' // 字符串操作工具，用于高效的代码转换
import dts from 'rollup-plugin-dts' // Rollup TypeScript 声明文件插件

// 检查临时声明文件目录是否存在
// 这些文件是通过 TypeScript 编译器预先生成的中间产物
if (!existsSync('temp/packages')) {
  console.warn(
    '未找到临时声明文件。请先运行 `tsc -p tsconfig.build-browser.json && tsc -p tsconfig.build-node.json`。',
  )
  process.exit(1)
}

// 读取所有需要处理的包
const packages = readdirSync('temp/packages') // 从临时目录读取所有已编译的包

// 处理目标包过滤
// 可以通过 TARGETS 环境变量指定只处理特定的包
const targets = process.env.TARGETS ? process.env.TARGETS.split(',') : null
const targetPackages = targets
  ? packages.filter(pkg => targets.includes(pkg)) // 过滤出指定的目标包
  : packages // 处理所有包

// 导出每个包的 Rollup 配置
// 为每个目标包生成一个独立的 Rollup 配置对象
export default targetPackages.map(
  /** @returns {import('rollup').RollupOptions} */
  pkg => {
    return {
      // 输入：每个包临时目录中的主声明文件
      input: `./temp/packages/${pkg}/src/index.d.ts`,

      // 输出：合并后的单一声明文件
      output: {
        file: `packages/${pkg}/dist/${pkg}.d.ts`, // 输出到包的 dist 目录
        format: 'es', // ES 模块格式
      },

      // 插件配置
      plugins: [
        dts(), // TypeScript 声明文件处理插件
        patchTypes(pkg), // 自定义类型修补插件
        ...(pkg === 'vue' ? [copyMts()] : []), // Vue 主包需要额外的 .mts 文件复制
      ],

      // 警告处理器
      onwarn(warning, warn) {
        // 在 dts rollup 过程中，所有依赖默认都被外部化处理
        // 忽略非相对路径的未解析导入警告（这些是正常的外部依赖）
        if (
          warning.code === 'UNRESOLVED_IMPORT' &&
          !warning.exporter?.startsWith('.') // 相对路径导入仍需要警告
        ) {
          return // 忽略此类警告
        }
        warn(warning) // 输出其他警告
      },
    }
  },
)

/**
 * 修补由 rollup-plugin-dts 生成的声明文件
 * 主要功能：
 * 1. 将所有类型转换为内联导出，并从大的 export {} 声明中移除它们
 *    否则在 vitepress 中调用 defineComponent 时会出现问题
 *    报错："无法命名推断类型，因为没有引用"
 * 2. 附加包特定的类型扩展（jsx、宏 等）
 *
 * @param {string} pkg 包名
 * @returns {import('rollup').Plugin} Rollup 插件对象
 */
function patchTypes(pkg) {
  return {
    name: 'patch-types', // 插件名称

    // 在代码块渲染阶段处理声明文件
    renderChunk(code, chunk) {
      // 创建 MagicString 实例，用于高效的字符串操作
      const s = new MagicString(code)

      // 使用 Babel 解析器解析 TypeScript 代码为 AST
      const ast = parse(code, {
        plugins: ['typescript'], // 启用 TypeScript 解析插件
        sourceType: 'module', // 模块类型
      })

      /**
       * 处理声明节点
       * 将需要导出的类型声明转换为内联导出形式
       *
       * @param {import('@babel/types').VariableDeclarator | import('@babel/types').TSTypeAliasDeclaration | import('@babel/types').TSInterfaceDeclaration | import('@babel/types').TSDeclareFunction | import('@babel/types').TSInterfaceDeclaration | import('@babel/types').TSEnumDeclaration | import('@babel/types').ClassDeclaration} node AST 节点
       * @param {import('@babel/types').VariableDeclaration} [parentDecl] 父级声明节点（对于变量声明）
       */
      function processDeclaration(node, parentDecl) {
        // 检查节点是否有 ID
        if (!node.id) {
          return
        }

        // 确保 ID 是标识符类型
        assert(node.id.type === 'Identifier')
        const name = node.id.name

        // 跳过以下划线开头的内部类型（不导出）
        if (name.startsWith('_')) {
          return
        }

        // 标记该声明需要从集中导出中移除
        shouldRemoveExport.add(name)

        // 如果该声明在集中导出中，则在其声明前添加 export 关键词
        if (isExported.has(name)) {
          const start = (parentDecl || node).start // 获取声明的起始位置
          assert(typeof start === 'number')
          s.prependLeft(start, `export `) // 在声明前添加 export
        }
      }

      // 存储已导出的类型名称
      const isExported = new Set()
      // 存储需要从集中导出中移除的类型名称
      const shouldRemoveExport = new Set()

      // 第一轮：检查所有已导出的类型
      // 遍历 AST 的所有顶层节点
      for (const node of ast.program.body) {
        // 查找命名导出声明（不包括从其他模块的重新导出）
        if (node.type === 'ExportNamedDeclaration' && !node.source) {
          // 遍历所有导出说明符
          for (let i = 0; i < node.specifiers.length; i++) {
            const spec = node.specifiers[i]
            if (spec.type === 'ExportSpecifier') {
              isExported.add(spec.local.name) // 记录已导出的本地名称
            }
          }
        }
      }

      // 第二轮：添加内联导出
      // 遍历所有声明，为需要导出的类型添加 export 关键词
      for (const node of ast.program.body) {
        if (node.type === 'VariableDeclaration') {
          // 处理变量声明（如 declare const 类型声明）
          processDeclaration(node.declarations[0], node)

          // 确保只有一个声明项（Vue 中的约定）
          if (node.declarations.length > 1) {
            assert(typeof node.start === 'number')
            assert(typeof node.end === 'number')
            throw new Error(
              `无法处理包含多个声明项的 declare const：\n${code.slice(
                node.start,
                node.end,
              )}`,
            )
          }
        } else if (
          // 处理其他类型的声明
          node.type === 'TSTypeAliasDeclaration' || // 类型别名声明
          node.type === 'TSInterfaceDeclaration' || // 接口声明
          node.type === 'TSDeclareFunction' || // 函数声明
          node.type === 'TSEnumDeclaration' || // 枚举声明
          node.type === 'ClassDeclaration' // 类声明
        ) {
          processDeclaration(node)
        }
      }

      // 第三轮：移除集中导出中的对应项
      // 因为已经改为内联导出，所以需要从集中导出中移除
      for (const node of ast.program.body) {
        if (node.type === 'ExportNamedDeclaration' && !node.source) {
          let removed = 0 // 记录已移除的导出项数量

          // 遍历所有导出说明符
          for (let i = 0; i < node.specifiers.length; i++) {
            const spec = node.specifiers[i]

            // 检查是否是需要移除的导出项
            if (
              spec.type === 'ExportSpecifier' &&
              shouldRemoveExport.has(spec.local.name)
            ) {
              assert(spec.exported.type === 'Identifier')
              const exported = spec.exported.name

              // 检查是否有别名导出（如 export { Foo as Bar }）
              if (exported !== spec.local.name) {
                // 对于别名导出，保留不处理。这种情况只会在以下情况中发生：
                //   type Foo
                //   export { Foo as Bar }
                continue
              }

              // 获取下一个导出项，用于计算移除范围
              const next = node.specifiers[i + 1]
              if (next) {
                // 不是最后一个：移除到下一个导出项的开头
                assert(typeof spec.start === 'number')
                assert(typeof next.start === 'number')
                s.remove(spec.start, next.start)
              } else {
                // 最后一个导出项：需要考虑分隔符
                const prev = node.specifiers[i - 1]
                assert(typeof spec.start === 'number')
                assert(typeof spec.end === 'number')
                s.remove(
                  prev
                    ? (assert(typeof prev.end === 'number'), prev.end) // 有前一个：从前一个的结尾开始移除
                    : spec.start, // 没有前一个：从当前开始移除
                  spec.end,
                )
              }
              removed++ // 已移除数量加一
            }
          }

          // 如果所有导出项都被移除了，则移除整个导出声明
          if (removed === node.specifiers.length) {
            assert(typeof node.start === 'number')
            assert(typeof node.end === 'number')
            s.remove(node.start, node.end)
          }
        }
      }

      // 应用所有修改
      code = s.toString()

      // 附加包特定的类型定义
      // 检查包是否有额外的类型文件目录
      const additionalTypeDir = `packages/${pkg}/types`
      if (existsSync(additionalTypeDir)) {
        // 读取所有额外的类型文件并附加到代码末尾
        code +=
          '\n' +
          readdirSync(additionalTypeDir)
            .map(file => readFileSync(`${additionalTypeDir}/${file}`, 'utf-8'))
            .join('\n')
      }

      return code // 返回修改后的代码
    },
  }
}

/**
 * 为 Vue 主包复制 .mts 声明文件
 *
 * 根据 TypeScript 4.7 的规定：
 * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing
 *
 * 为了正确地为 Node.js ESM 和 CJS 两种模块系统提供类型支持，
 * 需要有两个单独的声明文件：
 * - vue.d.ts：用于 CommonJS 模块和传统的 TypeScript 项目
 * - vue.d.mts：用于 ES 模块和现代的 TypeScript 项目
 *
 * 因此需要在构建过程中将 vue.d.ts 复制为 vue.d.mts
 *
 * @returns {import('rollup').Plugin} Rollup 插件对象
 */
function copyMts() {
  return {
    name: 'copy-vue-mts', // 插件名称

    // 在文件写入完成后执行
    writeBundle(_, bundle) {
      // 确保 vue.d.ts 文件存在且包含代码内容
      assert('code' in bundle['vue.d.ts'])

      // 将 vue.d.ts 的内容复制到 vue.d.mts 文件
      // 这两个文件的内容完全相同，只是文件扩展名不同
      // TypeScript 会根据文件扩展名自动选择合适的声明文件
      writeFileSync('packages/vue/dist/vue.d.mts', bundle['vue.d.ts'].code)
    },
  }
}
